package de.dnb.gnd.utils;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import javax.naming.OperationNotSupportedException;

import de.dnb.basics.applicationComponents.RangeCheckUtils;
import de.dnb.basics.applicationComponents.StringUtils;
import de.dnb.basics.filtering.FilterUtils;
import de.dnb.basics.filtering.IFunction;
import de.dnb.basics.filtering.IPredicate;
import de.dnb.gnd.exceptions.IllFormattedLineException;
import de.dnb.gnd.parser.Format;
import de.dnb.gnd.parser.Indicator;
import de.dnb.gnd.parser.Subfield;
import de.dnb.gnd.parser.line.Line;
import de.dnb.gnd.parser.line.LineParser;
import de.dnb.gnd.parser.tag.BibTagDB;
import de.dnb.gnd.parser.tag.GNDTagDB;
import de.dnb.gnd.parser.tag.Tag;
import de.dnb.gnd.parser.tag.TagDB;

/**
 * Enthält die Utilities, die mit Unterfeldern und Zeilen zu tun haben.
 * @author baumann
 *
 */
public final class SubfieldUtils {

    private SubfieldUtils() {
    }

    public static final IFunction<Subfield, Character> FUNCTION_SUBFIELD_TO_INDICATOR_CHAR =
        new IFunction<Subfield, Character>() {
            @Override
            public Character apply(final Subfield subfield) {
                return subfield.getIndicator().indicatorChar;
            }
        };
    public static final IFunction<Indicator, Character> FUNCTION_INDICATOR_TO_CHAR =
        new IFunction<Indicator, Character>() {
            @Override
            public Character apply(final Indicator indicator) {
                return indicator.indicatorChar;
            }
        };
    public static final IFunction<Subfield, String> FUNCTION_SUBFIELD_TO_CONTENT =
        new IFunction<Subfield, String>() {
            @Override
            public String apply(final Subfield subfield) {
                return subfield.getContent();
            }
        };

    public static boolean containsIndicator(
        final char indicator,
        Collection<Indicator> indicators) {
        IPredicate<Indicator> predicate = new IPredicate<Indicator>() {
            @Override
            public boolean accept(Indicator element) {
                return element.indicatorChar == indicator;
            }
        };
        return FilterUtils.contains(indicators, predicate);
    }

    /**
     * Ist der Indikator ind in der Zeile enthalten?
     * 
     * @param line	nicht null.
     * @param ind	beliebig.
     * @return		true, wenn in line enthalten.
     */
    public static boolean containsIndicator(final Line line, final char ind) {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        return SubfieldUtils.getFirstSubfield(line, ind) != null;
    }

    //@formatter:off
	/**
	 * enthält die Zeile ein Unterfeld mit Indikator indicator?
	 * @param line		nicht null.
	 * @param indicator	nicht null.
	 * @return			Enthalten?
	 */
	public static boolean containsIndicator(
		final Line line,
		final Indicator indicator) {
		RangeCheckUtils.assertReferenceParamNotNull("line", line);
		RangeCheckUtils.assertReferenceParamNotNull("indicator", indicator);
		return 
			getFirstSubfield(line.getSubfields(Format.PICA3), indicator) 
				!= null;
	}

	public static boolean containsIndicatorInSubfields(
		final char indicator,
		Collection<Subfield> subfields) {
		IPredicate<Subfield> predicate = new IPredicate<Subfield>() {
			@Override
			public boolean accept(Subfield element) {
				return element.getIndicator().indicatorChar == indicator;
			}
		};
		return FilterUtils.contains(subfields, predicate);
	}

	/**
	 * Liefert den Inhalt des ersten Subfelds mit Indikator ind, sonst null.
	 * @param line	nicht null.
	 * @param ind	beliebig.
	 * @return		Inhalt des ersten Unterfelds oder null.
	 */
	public static String getContentOfFirstSubfield(
		final Line line,
		final char ind) {
		RangeCheckUtils.assertReferenceParamNotNull("line", line);
		Subfield subfield = getFirstSubfield(line, ind);
		if (subfield != null)
			return subfield.getContent();
		else
			return null;
	}

	//@formatter:on
    /**
     * Liefert zu einer Menge von Zeilen und einem Indikator alle Inhalte
     * der ersten Unterfelder zu diesem Indikator. Ergebnis kann als Spalte 
     * einer Tabelle aufgefasst werden.
     * 
     * @param lines		nicht null.
     * @param indicator	beliebig.
     *
     * @return			nicht null, eventuell leer.
     */
    public static List<String> getContentsOfFirstSubfields(
        final Iterable<Line> lines,
        final char indicator) {
        RangeCheckUtils.assertReferenceParamNotNull("lines", lines);
        final Collection<Subfield> subfields =
            RecordUtils.getFirstSubfields(lines, indicator);
        return SubfieldUtils.getContentsOfSubfields(subfields);
    }

    /**
     * Liefert zu einer Menge von Unterfeldern alle 
     * Inhalte der Unterfelder. 
     * 
     * @param subfields		nicht null.
     * @return				nicht null, aber eventuell leer.
     */
    public static List<String> getContentsOfSubfields(
        final Iterable<Subfield> subfields) {
        RangeCheckUtils.assertReferenceParamNotNull("subfields", subfields);
        return FilterUtils.map(subfields,
            SubfieldUtils.FUNCTION_SUBFIELD_TO_CONTENT);
    }

    /**
     * Liefert zu Zeile alle Inhalte der in der Zeile enthaltenen Unterfelder. 
     * 
     * @param line			nicht null.
     * @return				nicht null, aber eventuell leer.
     */
    public static List<String> getContentsOfSubfields(final Line line) {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        Collection<Subfield> subfields = line.getSubfields();
        return getContentsOfSubfields(subfields);
    }

    /**
     * Liefert das erste Subfeld mit Indikator ind, sonst null.
     * @param subfields	nicht null.
     * @param ind		beliebig.
     * @return			Erstes Unterfeld oder null.
     */
    public static Subfield getFirstSubfield(
        final Iterable<Subfield> subfields,
        final char ind) {
        RangeCheckUtils.assertReferenceParamNotNull("subfields", subfields);
        final IPredicate<Subfield> predicate = new IPredicate<Subfield>() {
            @Override
            public boolean accept(final Subfield subfield) {
                return subfield.getIndicator().indicatorChar == ind;
            }
        };
        return FilterUtils.find(subfields, predicate);
    }

    /**
     * Liefert den Inhalt des ersten Subfeld mit Indikator ind, sonst null.
     * @param subfields	nicht null.
     * @param ind		beliebig.
     * @return			Inhalt des ersten Unterfeld oder null.
     */
    public static String getContentOfFirstSubfield(
        final Iterable<Subfield> subfields,
        final char ind) {
        RangeCheckUtils.assertReferenceParamNotNull("subfields", subfields);
        Subfield subfield = getFirstSubfield(subfields, ind);
        if (subfield == null)
            return null;
        else
            return subfield.getContent();
    }

    /**
     * Liefert das erste Subfeld mit Indikator indicator, sonst null.
     * @param subfields	nicht null.
     * @param indicator	nicht null.
     * @return			Erstes Unterfeld oder null.
     */
    public static Subfield getFirstSubfield(
        final Iterable<Subfield> subfields,
        final Indicator indicator) {
        RangeCheckUtils.assertReferenceParamNotNull("subfields", subfields);
        RangeCheckUtils.assertReferenceParamNotNull("indicator", indicator);
        final IPredicate<Subfield> predicate = new IPredicate<Subfield>() {
            @Override
            public boolean accept(final Subfield subfield) {
                return subfield.getIndicator() == indicator;
            }
        };
        return FilterUtils.find(subfields, predicate);
    }

    /**
     * Liefert das erste Subfeld mit Indikator ind, sonst null.
     * @param line	nicht null.
     * @param ind	beliebig.
     * @return		Erstes Unterfeld oder null.
     */
    public static Subfield getFirstSubfield(final Line line, final char ind) {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        final Collection<Subfield> subfields = line.getSubfields();
        return SubfieldUtils.getFirstSubfield(subfields, ind);
    }

    /**
     * Liefert das erste Subfeld mit Indikator indicator, sonst null.
     * @param line	nicht null.
     * @param indicator	nicht null.
     * @return			Erstes Unterfeld oder null.
     */
    public static Subfield getFirstSubfield(
        final Line line,
        final Indicator indicator) {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        RangeCheckUtils.assertReferenceParamNotNull("indicator", indicator);
        return getFirstSubfield(line.getSubfields(Format.PICA3), indicator);
    }

    /**
     * Liefert zu einer Zeile und einem Indikator alle
     * Unterfelder mit diesem Indikator. Ergebnis kann als gefilterte Zeile 
     * einer Tabelle aufgefasst werden.
     * 
     * @param line		nicht null.
     * @param indicator	beliebig.
     *
     * @return			Neue Liste, nicht null, modifizierbar.
     */
    public static List<Subfield> getSubfields(
        final Line line,
        final char indicator) {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        return SubfieldUtils.getSubfieldsFromCollection(line.getSubfields(),
            indicator);
    }

    /**
     * Liefert zu einer Zeile und einem Indikator alle
     * Unterfelder mit diesem Indikator. Ergebnis kann als gefilterte Zeile 
     * einer Tabelle aufgefasst werden.
     * 
     * @param line		nicht null.
     * @param indicator	beliebig.
     *
     * @return			Neue Liste, nicht null, modifizierbar.
     */
    public static List<String> getContentsOfSubfields(
        final Line line,
        final char indicator) {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        return getContentsOfSubfields(getSubfields(line, indicator));
    }

    /**
     * Liefert zu einer Menge von Unterfeldern und einem Indikator alle
     * Unterfelder zu diesem Indikator. Ergebnis kann als gefilterte Zeile 
     * einer Tabelle aufgefasst werden.
     * 
     * @param subfields		nicht null.
     * @param indicator		beliebig.
     *
     * @return				Neue Liste, nicht null.
     */
    public static List<Subfield> getSubfieldsFromCollection(
        final Iterable<Subfield> subfields,
        final char indicator) {
        RangeCheckUtils.assertReferenceParamNotNull("subfields", subfields);
        final IPredicate<Subfield> predicate = new IPredicate<Subfield>() {
            @Override
            public boolean accept(final Subfield subfield) {
                return subfield.getIndicator().indicatorChar == indicator;
            }
        };
        return FilterUtils.newFilteredList(subfields, predicate);
    }

    /**
     * Liefert eine Liste von Unterfeldern aus einer Zeile ohne
     * störende Unterfelder $4, $5, $v ...
     * 
     * @param 	line	nicht null.
     * @return			nicht null.
     */
    public static List<Subfield> getNamingRelevantSubfields(final Line line) {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        List<Subfield> subfields = line.getSubfields();
        List<Character> irrelevantIndicators =
            Arrays.asList('4', '5', 'v', 'X', 'Y', 'Z', 'L', 'u', 'S', '0',
                '2', 'T', 'U', '8', '9');
        subfields =
            SubfieldUtils.removeSubfieldsFromCollection(subfields,
                irrelevantIndicators);
        return subfields;
    }

    /**
     * Entfernt aus  einer Menge von Unterfeldern und einem Indikator alle
     * Unterfelder zu diesem Indikator. Ergebnis kann als gefilterte Zeile 
     * einer Tabelle aufgefasst werden.
     * 
     * @param subfields		nicht null.
     * @param indicator		beliebig.
     *
     * @return				Neue Liste, nicht null.
     */
    public static List<Subfield> removeSubfieldFromCollection(
        final Iterable<Subfield> subfields,
        final Indicator indicator) {
        RangeCheckUtils.assertReferenceParamNotNull("subfields", subfields);
        final IPredicate<Subfield> predicate = new IPredicate<Subfield>() {
            @Override
            public boolean accept(final Subfield subfield) {
                return subfield.getIndicator() != indicator;
            }
        };
        return FilterUtils.newFilteredList(subfields, predicate);
    }

    /**
     * Entfernt aus einer Zeile und einem Indikator alle
     * Unterfelder mit diesem Indikator. Ergebnis kann als gefilterte Zeile 
     * einer Tabelle aufgefasst werden.
     * 
     * @param line			nicht null.
     * @param indicators	beliebig.
     *
     * @return			 	nicht null.
     */
    public static List<Subfield> removeSubfields(
        final Line line,
        final Character... indicators) {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        return removeSubfieldsFromCollection(line.getSubfields(), indicators);
    }

    /**
     * Entfernt aus  einer Menge von Unterfeldern und einem Indikator alle
     * Unterfelder zu diesem Indikator. Ergebnis kann als gefilterte Zeile 
     * einer Tabelle aufgefasst werden.
     * 
     * @param subfields		nicht null.
     * @param indicators		beliebig.
     *
     * @return				Neue Liste, nicht null.
     */
    public static List<Subfield> removeSubfieldsFromCollection(
        final Iterable<Subfield> subfields,
        final Character... indicators) {
        RangeCheckUtils.assertReferenceParamNotNull("subfields", subfields);
        List<Character> indicatorCollection = Arrays.asList(indicators);
        return SubfieldUtils.removeSubfieldsFromCollection(subfields,
            indicatorCollection);
    }

    /**
     * Entfernt aus  einer Menge von Unterfeldern und einem Indikator alle
     * Unterfelder zu diesem Indikator. Ergebnis kann als gefilterte Zeile 
     * einer Tabelle aufgefasst werden.
     * 
     * @param subfields		nicht null.
     * @param indicators	nicht null.
     *
     * @return				Neue Liste, nicht null.
     */
    public static List<Subfield> removeSubfieldsFromCollection(
        final Iterable<Subfield> subfields,
        final Collection<Character> indicators) {
        RangeCheckUtils.assertReferenceParamNotNull("subfields", subfields);
        RangeCheckUtils.assertReferenceParamNotNull("indicators", indicators);
        final IPredicate<Subfield> predicate = new IPredicate<Subfield>() {
            @Override
            public boolean accept(final Subfield subfield) {
                return !indicators
                    .contains(subfield.getIndicator().indicatorChar);
            }
        };
        return FilterUtils.newFilteredList(subfields, predicate);
    }

    /**
     * Gibt eine Zeile mit ersetzten Unterfeldern oder null. Für die
     * Ersetzung der Unterfelder ist die equals()-Relation massgeblich.
     * 
     * @param line			nicht null.
     * @param original		nicht null.
     * @param replacement	nicht null.
     * @return				Neue Zeile oder die alte, wenn nichts 
     * 						ersetzt wurde,
     * @throws IllFormattedLineException
     * 						Wenn die neue Zeile nicht zulässig ist.
     */
    public static Line replaceAll(
        final Line line,
        final Subfield original,
        final Subfield replacement) throws IllFormattedLineException {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        RangeCheckUtils.assertReferenceParamNotNull("original", original);
        RangeCheckUtils.assertReferenceParamNotNull("replacement", replacement);
        final Tag tag = line.getTag();
        final List<Subfield> subfields = line.getSubfields();
        final boolean repl =
            Collections.replaceAll(subfields, original, replacement);
        if (repl) {
            final Line newLine = LineParser.parse(tag, subfields);
            return newLine;
        } else {
            return line;
        }

    }

    /**
     * Behält aus  einer Menge von Unterfeldern und einer Indikatorlist alle
     * Unterfelder zu diesem Indikator bei. Ergebnis kann als gefilterte Zeile 
     * einer Tabelle aufgefasst werden.
     * 
     * @param subfields		nicht null.
     * @param indicators	beliebig.
     *
     * @return				Neue Liste, nicht null.
     */
    public static List<Subfield> retainSubfields(
        final Iterable<Subfield> subfields,
        final Character... indicators) {
        RangeCheckUtils.assertReferenceParamNotNull("subfields", subfields);
        RangeCheckUtils.assertReferenceParamNotNull("indicators", indicators);
        return retainSubfields(subfields, Arrays.asList(indicators));
    }

    /**
     * Behält aus  einer Menge von Unterfeldern und einer Indikatorlist alle
     * Unterfelder zu diesem Indikator bei. Ergebnis kann als gefilterte Zeile 
     * einer Tabelle aufgefasst werden.
     * 
     * @param subfields		nicht null.
     * @param indicators	beliebig.
     *
     * @return				Neue Liste, nicht null.
     */
    public static List<Subfield> retainSubfields(
        final Iterable<Subfield> subfields,
        final Collection<Character> indicators) {
        RangeCheckUtils.assertReferenceParamNotNull("subfields", subfields);
        RangeCheckUtils.assertReferenceParamNotNull("indicators", indicators);
        final IPredicate<Subfield> predicate = new IPredicate<Subfield>() {
            @Override
            public boolean accept(final Subfield subfield) {
                return indicators
                    .contains(subfield.getIndicator().indicatorChar);
            }
        };
        return FilterUtils.newFilteredList(subfields, predicate);
    }

    /**
     * Liefert zu einer Zeile und einer Indikatorliste alle
     * Unterfelder zu diesen Indikatoren. Ergebnis kann als gefilterte Zeile 
     * einer Tabelle aufgefasst werden. Reihenfolge Pica3.
     * 
     * @param line		nicht null.
     * @param indicators	beliebig
     *
     * @return				Neue Liste, nicht null.
     */
    public static List<Subfield> retainSubfields(
        final Line line,
        final Character... indicators) {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        RangeCheckUtils.assertReferenceParamNotNull("indicators", indicators);
        List<Subfield> list = line.getSubfields();
        return retainSubfields(list, Arrays.asList(indicators));
    }

    /**
     * Sind die beiden Listen gleich, wenn man indicators NICHT berücksichtigt?
     * 
     * @param subfields1	nicht null
     * @param subfields2	nicht null
     * @param indicators	nicht null
     * 
     * @return	true, wenn Listen gleich
     */
    public static boolean equalsRemoving(
        final Iterable<Subfield> subfields1,
        final Iterable<Subfield> subfields2,
        final Character... indicators) {
        RangeCheckUtils.assertReferenceParamNotNull("subfields1", subfields1);
        RangeCheckUtils.assertReferenceParamNotNull("subfields2", subfields2);
        RangeCheckUtils.assertReferenceParamNotNull("indicators", indicators);
        List<Subfield> list1 =
            removeSubfieldsFromCollection(subfields1, indicators);
        List<Subfield> list2 =
            removeSubfieldsFromCollection(subfields2, indicators);
        return list1.equals(list2);
    }

    /**
     * Sind die Unterfelder gleich, wenn man indicators NICHT berücksichtigt?
     * 
     * @param line			nicht null
     * @param subfields2	nicht null
     * @param indicators	nicht null
     * 
     * @return	true, wenn Listen gleich
     */
    public static boolean equalsRemoving(
        final Line line,
        final Iterable<Subfield> subfields2,
        final Character... indicators) {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        RangeCheckUtils.assertReferenceParamNotNull("subfields2", subfields2);
        RangeCheckUtils.assertReferenceParamNotNull("indicators", indicators);
        return equalsRemoving(line.getSubfields(), subfields2, indicators);
    }

    /**
     * Sind die Unterfelder gleich, wenn man indicators NICHT berücksichtigt?
     * 
     * @param line1			nicht null
     * @param line2			nicht null
     * @param indicators	nicht null
     * 
     * @return	true, wenn Listen gleich (die Tags werden ignoriert!)
     */
    public static boolean equalsRemoving(
        final Line line1,
        final Line line2,
        final Character... indicators) {
        RangeCheckUtils.assertReferenceParamNotNull("line1", line1);
        RangeCheckUtils.assertReferenceParamNotNull("line2", line1);
        RangeCheckUtils.assertReferenceParamNotNull("indicators", indicators);
        return equalsRemoving(line1.getSubfields(), line2.getSubfields(),
            indicators);
    }

    /**
     * Sind die beiden Listen gleich, wenn man NUR indicators berücksichtigt?
     * 
     * @param subfields1	nicht null
     * @param subfields2	nicht null
     * @param indicators	nicht null
     * 
     * @return	true, wenn Listen gleich
     */
    public static boolean equalsRetaining(
        final Iterable<Subfield> subfields1,
        final Iterable<Subfield> subfields2,
        final Character... indicators) {
        RangeCheckUtils.assertReferenceParamNotNull("subfields1", subfields1);
        RangeCheckUtils.assertReferenceParamNotNull("subfields2", subfields2);
        RangeCheckUtils.assertReferenceParamNotNull("indicators", indicators);
        List<Subfield> list1 = retainSubfields(subfields1, indicators);
        List<Subfield> list2 = retainSubfields(subfields2, indicators);
        return list1.equals(list2);
    }

    /**
     * Sind die Unterfelder gleich, wenn man wenn man 
     * NUR indicators berücksichtigt?
     * 
     * @param line			nicht null
     * @param subfields2	nicht null
     * @param indicators	nicht null
     * 
     * @return	true, wenn Listen gleich
     */
    public static boolean equalsRetaining(
        final Line line,
        final Iterable<Subfield> subfields2,
        final Character... indicators) {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        RangeCheckUtils.assertReferenceParamNotNull("subfields2", subfields2);
        RangeCheckUtils.assertReferenceParamNotNull("indicators", indicators);
        return equalsRetaining(line.getSubfields(), subfields2, indicators);
    }

    /**
     * Sind die Unterfelder gleich, wenn man NUR indicators berücksichtigt?
     * 
     * @param line1			nicht null
     * @param line2			nicht null
     * @param indicators	nicht null
     * 
     * @return	true, wenn Listen gleich (die Tags werden ignoriert!)
     */
    public static boolean equalsRetaining(
        final Line line1,
        final Line line2,
        final Character... indicators) {
        RangeCheckUtils.assertReferenceParamNotNull("line1", line1);
        RangeCheckUtils.assertReferenceParamNotNull("line2", line1);
        RangeCheckUtils.assertReferenceParamNotNull("indicators", indicators);
        return equalsRetaining(line1.getSubfields(), line2.getSubfields(),
            indicators);
    }

    /**
     * Gibt ein neues Unterfeld $g mit Inhalten der ursprünglichen
     * Unterfelder durch ",_" aneinandergereiht.
     * 
     * @param subfields nicht null
     * @return          null, wenn Fehler
     */
    public static Subfield mergeDollarG(final Collection<Subfield> subfields) {
        RangeCheckUtils.assertReferenceParamNotNull("subfields", subfields);
        String s =
            StringUtils.concatenate(",  ", subfields,
                IFSubfieldToContent.FUNCTION);
        Subfield subfield;
        try {
            subfield = new Subfield(GNDTagDB.DOLLAR_G, s);
            return subfield;
        } catch (IllFormattedLineException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            return null;
        }
    }

    /**
     * @param args
     * @throws IllFormattedLineException 
     * @throws OperationNotSupportedException 
     * @throws IOException 
     */
    public static void main(final String[] args)
        throws IllFormattedLineException,
        OperationNotSupportedException,
        IOException {
        TagDB db = BibTagDB.getDB();
        Line line1 =
            LineParser.parse(
                "0604 ListeNSW$bKBNL-online (J)$cwww$ek$ep$et$D12-09-01", db,
                false);
        System.out.println(getContentsOfSubfields(line1, 'e'));
    }

}
